﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using MediaRecommender.Model;

namespace MediaRecommender.Controller
{
    public class GenerateRecommendationsController
    {

        private string logName = "generateRecommendations" + Convert.ToString(DateTime.Now).Replace(":", ";").Replace("\\", "-").Replace("/", "-");

        public GenerateRecommendationsController()
        { }

        public ICollection<Media> CreateRecommendations(User user)
        {
            logName += user.Name + ".txt";
            // Generates Collaboraitve recommendations based on an user
            List<Media> CollaborativeMedia = (List<Media>)CollaborativeRecommender(user);

            // Generates Content-Based recommendations based on an user
            List<Media> ContentBasedMedia = (List<Media>)ContentBasedRecommender(user);

            // Merges the two recommendation collections, and returns it as the recommendations given to the user
            return MergeMediaCollections(CollaborativeMedia, ContentBasedMedia, user);
        }

        #region CollaborativeRecommender
        // Finds users to use in the collaborative recommendation process, and serves as a gateway to the rest of the algorithm
        private ICollection<Media> CollaborativeRecommender(User user)
        {
            Dictionary<User, double> IndexedUsers = new Dictionary<User, double>();
            if (user != null)
            {
                List<User> allUsers = (List<User>)UserCollection.Instance.GetAllDifferentUsers(user.Name);
                IndexedUsers = IndexUsers(user, allUsers);

                return GenerateMedia(IndexedUsers, user);
            }
            else return GenerateMedia(IndexedUsers, user);
        }

        // Uses CompareUserPair to see how much a user is similar to the user it is currently generating recommendations for
        private Dictionary<User, double> IndexUsers(User mainUser, List<User> users)
        {
            Dictionary<User, double> comparedUsers = new Dictionary<User, double>();
            double k;

            WriteToLog.WriteLog(mainUser.Name + " pearson index for rest of users: ", logName);
            foreach (User user in users)
            {
                if (user.Name != mainUser.Name)
                {
                    k = CompareUserPair(mainUser, user);
                    comparedUsers.Add(user, k);
                    WriteToLog.WriteLog(user.Name + " has coefficent: " + Convert.ToString(k), logName);
                }
            }
            WriteToLog.WriteLog("******************* END OF PEARSON************", logName);
            return comparedUsers;
        }

        // Compares two users based on the their respective ratings on media they've both consumed
        private double CompareUserPair(User mainUser, User user)
        {
            List<int> mainRating = new List<int>();
            List<int> userRating = new List<int>();
            int x = 0, y = 0, xy = 0, x2 = 0, y2 = 0;

            foreach (Media media in user.MediaList.Keys)
            {
                if (mainUser.MediaList.ContainsKey(media))
                {
                    mainRating.Add(mainUser.MediaList[media]);
                    userRating.Add(user.MediaList[media]);
                }
            }

            if (mainRating.Count() < 3)
            {
                return 0;
            }

            for (int i = 0; i < mainRating.Count; i++)
            {
                x += mainRating[i];
                y += userRating[i];
                xy += mainRating[i] * userRating[i];
                x2 += (int)Math.Pow(mainRating[i], 2);
                y2 += (int)Math.Pow(userRating[i], 2);
            }

            return Pearson(x, y, xy, x2, y2, mainRating.Count());
        }

        // Calculates a Pearson correlation coefficient
        private double Pearson(double x, double y, double xy, double x2, double y2, double n)
        {
            return ((n * xy) - (x * y)) / Math.Sqrt(((n * x2) - Math.Pow(x, 2)) * ((n * y2) - Math.Pow(y, 2)));
        }

        // Sorts users by the value given to them, and removes with no or negative relation
        private ICollection<Media> GenerateMedia(Dictionary<User, double> sortedUsers, User userToRec)
        {
            List<User> releUsers = new List<User>();

            sortedUsers = sortedUsers.OrderByDescending(x => x.Value).ToDictionary(x => x.Key, x => x.Value);

            foreach (var actualUser in sortedUsers)
            {
                if (actualUser.Value > 0)
                {
                    releUsers.Add(actualUser.Key);
                }
            }

            return FindCollaborativeMedia(releUsers, userToRec);
        }

        // Generates recommendations starting with the most related users
        private ICollection<Media> FindCollaborativeMedia(List<User> releUsers, User userToRec)
        {
            List<Media> recMedia = new List<Media>();

            int mediaCounter = 0;
            int tempIndex;
            int indexBoost = 1;

            foreach (User actualUser in releUsers)
            {
                Dictionary<Media, int> currMediaList = actualUser.MediaList.UserMediaList;

                foreach (KeyValuePair<Media, int> media in currMediaList)
                {
                    // Ignores all media which the main users allready have
                    if (media.Value >= 5 && !userToRec.MediaList.ContainsKey(media.Key))
                    {
                        // If the same media appears again, it will move up in the recommendation list
                        if (recMedia.Contains(media.Key))
                        {
                            tempIndex = recMedia.FindIndex(x => x == media.Key);

                            if (tempIndex > 1)
                            {
                                // Provides another boost if the media is from a friend
                                if (userToRec.Friends.Contains(actualUser))
                                {
                                    indexBoost++;
                                }

                                recMedia.Remove(media.Key);
                                recMedia.Insert(tempIndex - indexBoost, media.Key);

                                indexBoost = 1;
                            }
                        }
                        // Add media not already on the recommendation list
                        else
                        {
                            // Boost if the user is a friend
                            if (userToRec.Friends.Contains(actualUser))
                                recMedia.Insert(recMedia.Count - 1, media.Key);
                            else
                                recMedia.Add(media.Key);
                            mediaCounter++;
                        }
                    }
                    if (mediaCounter >= 6) break;
                }
                if (mediaCounter >= 6) break;
            }

            return recMedia;
        }
        #endregion

        #region ContentBasedRecommender
        private ICollection<Media> ContentBasedRecommender(User user)
        {
            // Gets all media available to the system
            List<Media> allMedia = MediaCollection.Instance.GetAllMedia();

            // Indexes the media based on the taste of a certain user
            Dictionary<Media, double> indexedMedia = IndexMedia(user, allMedia);

            // Finds and returns media based on relevancy scale
            return FindContentMedia(indexedMedia);
        }

        public Dictionary<Media, double> IndexMedia(User mainUser, List<Media> allMedia)
        {
            Dictionary<Media, double> indexedMedia = new Dictionary<Media, double>();
            double k, userLength = 0;
            List<double> userVector = new List<double>();
            List<bool> mediaVector;

            userVector.AddRange(mainUser.MediaList.VectorRepresentation);
            userVector.AddRange(mainUser.MediaList.AssociatedPeopleVector);

            // Finds the length of a users vector representation
            foreach (var variable in userVector)
            {
                userLength += Math.Pow(variable, (double)2);
            }

            WriteToLog.WriteLog(mainUser.Name + " coefficent for each media: ", logName);

            // Discards any media alrady in the users watched media list
            foreach (var media in allMedia)
            {
                if (!mainUser.MediaList.ContainsKey(media))
                {
                    mediaVector = new List<bool>();
                    mediaVector.AddRange(media.VectorRepresentation);
                    mediaVector.AddRange(media.AssociatedPeopleVector);

                    // Compares the vector represenations of a user and a piece of media, returning a coefficient
                    k = CompareVectorRepresentations(userVector, mediaVector, userLength, media.VectorLength);
                    WriteToLog.WriteLog(media.ID + " has coeficent: " + Convert.ToString(k), logName);

                    // Adds the media if it had any relevance
                    if (k != 0)
                        indexedMedia.Add(media, k);
                }
            }
            WriteToLog.WriteLog("***********END OF CONTENTBASED RECOMMENDATIONS***************", logName);
            return indexedMedia;
        }

        private double CompareVectorRepresentations(List<double> userRepresentation, List<bool> mediaRepresentation, double userLength, double mediaLength)
        {
            double commonPoints = 0;

            // Finds and adds all common points between a piece of media and a user
            // The Vector Representations is a list of numbers, with a weighted number indicating an item has a certain property, and
            // a certain weight, and a 0 if it doesn't have that property
            for (int i = 0; i < mediaRepresentation.Count; i++)
            {
                if (userRepresentation[i] != 0 && mediaRepresentation[i])
                {
                    commonPoints += userRepresentation[i];
                }
            }
            // If there was no common points, return 0
            if (commonPoints == 0)
                return 0;
            // Calculates the coefficient between the user and the media
            return FindCosineDistance(commonPoints, userLength, mediaLength);
        }

        private double FindCosineDistance(double common, double user, double media)
        {
            return common / Math.Sqrt(user * media);
        }

        private List<Media> FindContentMedia(Dictionary<Media, double> indexedMedia)
        {
            // Sorts the collection of media to place the highest valued media in the top
            indexedMedia = indexedMedia.OrderByDescending(x => x.Value).ToDictionary(x => x.Key, x => x.Value);

            List<Media> recMedia = new List<Media>();

            // Take the 20 highest media items from the list, and return them
            foreach (var media in indexedMedia.Keys)
            {
                recMedia.Add(media);
                if (recMedia.Count >= 6)
                    break;
            }

            return recMedia;
        }
        #endregion

        #region Merge
        private ICollection<Media> MergeMediaCollections(List<Media> collaborativeList, List<Media> contentbasedList, User user)
        {
            List<Media> mergedMedia = new List<Media>();
            int tempIndex, genreBoost = 0;

            // Merge the two lists until one of them runs out of items
            // Ignores media if they're already in the merged list
            while (contentbasedList.Count != 0 && collaborativeList.Count != 0)
            {
                if (!mergedMedia.Contains(contentbasedList[0]))
                    mergedMedia.Add(contentbasedList[0]);
                if (!mergedMedia.Contains(collaborativeList[0]))
                    mergedMedia.Add(collaborativeList[0]);

                contentbasedList.RemoveAt(0);
                collaborativeList.RemoveAt(0);
            }

            if (collaborativeList.Count == 0)
            {
                foreach (Media media in contentbasedList)
                    mergedMedia.Add(media);
            }
            else
            {
                foreach (Media media in collaborativeList)
                    mergedMedia.Add(media);
            }

            // Go through every media in the merged list
            for (int i = 0; i < mergedMedia.Count; i++)
            {
                genreBoost = 0;
                // Go through every genre the user preferes, and generate a boost based on it
                foreach (var genre in user.Preferences)
                {
                    if (mergedMedia[i].Genres.Contains(genre))
                        genreBoost++;
                }

                // Remove and insert media based on the boost it recieved from evaluating its genres
                if (genreBoost != 0)
                {
                    tempIndex = mergedMedia.FindIndex(x => x.Equals(mergedMedia[i]));
                    Media tempMedia = mergedMedia[i];
                    mergedMedia.Remove(mergedMedia[i]);
                    WriteToLog.WriteLog(user.Name + "'s media" + tempMedia.ID + " got a genres boost of " + Convert.ToString(genreBoost), logName);

                    if (genreBoost >= tempIndex)
                        mergedMedia.Insert(0, tempMedia);
                    else
                        mergedMedia.Insert(tempIndex - genreBoost, tempMedia);
                }
            }

            // Places media which occured in both lists at the top
            foreach (var media in collaborativeList)
            {
                if (contentbasedList.Contains(media))
                {
                    mergedMedia.Remove(media);
                    mergedMedia.Insert(0, media);
                }
            }
            WriteToLog.WriteLog("***********" + "END OF RECOMMENDATIONS FOR " + user.Name + "***************", logName);
            return mergedMedia;
        }
        #endregion
    }
}